Android — 事件分发机制(一)

前言

事件分发机制对于Android开发人员可以说是一个非常敏感的词汇,现在网上关于事件分发机制的讲解很多都是参考郭大叔的博客以及《Android开发艺术探索》。所以学习事件分发机制还是推荐看一下博文:

Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

强烈希望大家看以上博文,而忽略本文。

本菜鸡只是记录一下学习过程中比较重要的知识点。

初识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//注册点击事件
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "onClick execute");
}
});
//注册touch事件
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});

如果Button两个事件都注册了,那么哪一个事件会先执行呢?

事实证明,onTouch事件会优先于onClick事件执行,而且onTouch方法是有返回值的,如果返回true,onClick事件就不会执行了,也就是说这个事件被onTouch消费掉了,不会往下传递了。

我们知道,所有的控件都是继承至View的,只要你触摸了任何一个控件,就一定会调用该控件的dispatchTouchEvent()方法,源码:

1
2
3
4
5
6
7
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}

第一个条件:

mOnTouchListener变量是在setOnTouchListener()方法赋值的:

1
2
3
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}

也就是说只要我们注册了Touch事件,mOnTouchListener就一定被赋值了。

第二个条件:

是判断当前点击的空间是否是enable的,按钮类控件默认都是enable的,因此这个条件恒定为true。

第三个条件:

回调了mOnTouchListener.onTouch(this, event),如果这个方法返回true,则dispatchTouchEvent()返回true,就不会往下执行了。如果这个方法返回flase,就会去执行onTouchEvent(event)方法。

这也说明一个很重要的问题,那就是onClick的调用是在onTouchEvent()方法中。

onTouchEvent()源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}

switch判断中,当case为MotionEvent.ACTION_UP时,在经过种种判断之后进入performClick()方法中。performClick():

1
2
3
4
5
6
7
8
9
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}

可以看到只要mOnClickListener不为空,就会去调用它的onClick方法,而mOnClickListener又是在哪赋值的呢?其实和上面变量mOnTouchListener的赋值是一样的,即:

1
2
3
4
5
6
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
mOnClickListener = l;
}

也就是在注册点击事件的时候就会赋值,然后会在performClick()方法中回调被点击控件的onClick()方法。

然后,我们再来认识一下Touch事件的层级传递。

如果给一个控件注册了Touch事件每次点击它的时候都会触发一系列的ACTION_DWON,ACTION_MOVE,ACTION_UP等事件。这里需要注意的是,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其他的ACTION就不会在得到执行了,简单来说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个ACTION返回true,才会触发后一个ACTION。

说到这里,其实要注意一点,如果在onTouch方法中返回了false,则会进入onTouchEvent()方法中,这里onTouchEvent()也是可以返回true的导致最终dispatchTouchEvent()方法返回true。主要在于控件是否是默认可点击的。

重点:

onTouch()和onTouchEvent()的区别

从源码中可以看出,两个方法都是在View的dispatchTouchEvent()中调用的,onTouch()优先于onTouchEvent()执行。如果在onTouch()方法中通过返回true将事件消费掉,onTouchEvent()将不会在执行。

另外需要注意的是,onTouch()方法能够执行需要两个前提条件,第一是mOnTouchListener的值不能为空,第二个是控件必须上可点击的。如果控件是不可点击的,则给它注册Touch事件将不会执行,对于这一类控件,如果我们想要监听它的事件,就必须通过重写onTouchEvent()方法来实现。

再识

Android中的Touch事件的传递,绝对是先传递到ViewGroup,在传递给View的。

在上面我们说过,只有你触摸了任何控件,都会去调用该控件的dispatchTouchEvent()方法,这个说法没错,但是不够完整。实际情况是,当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent()方法,然后在布局的dispatchTouchEvent()方法中找到被点击的相应控件,在去调用控件的dispatchTouchEvent()方法。

ViewGroup的dispatchTouchEvent()方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}

在第十三行我们可以看到一个条件判断,如果disallowIntercept和 !onInterceptTouchEvent()两者有一个true,就会进入这个条件判断。disallowIntercept是指是否禁用掉事件拦截功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent()方法对这个值进行修改。当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入条件判断的内部。第二个值是对onInterceptTouchEvent()方法的返回值取反。如果这个方法返回true,就进不去条件判断内部,按钮点击事件就都被屏蔽了,也就是按钮点击事件的处理是在条件判断内部进行的。

首先是通过一个for循环遍历当前ViewGroup下的所有的子View,然后在判断当前遍历的View是否是正在点击的View,如果是就是进入条件判断的内部,然后在调用该View的dispatchTouchEvent()。

这里需要注意的是,调用子View的dispatchTouchEvent()是有返回值的,我们已经知道,如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent()返回值必定为true,因此就会给ViewGroup的dispatchTouchEvent()方法直接返回了true,这也就导致后面的代码无法执行了。如果这个时候没有返回true,则会往下执行,如果target为null,则会进入条件判断内部,一般情况下target都是为null的,因此会调用super.dispatchTouchEvent()。

总结

  • Android 事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。
  • 在ViewGroup中可以通过onInterceptTouchEvent()方法对事件传递进行拦截。onInterceptTouchEvent()返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。
  • 子View中如果将传递的事件消费掉。ViewGroup中将无法接受到任何事件。
我们一直都向往,面朝大海,春暖花开。 但是几人能做到,心中有爱,四季不败?